package com.rdxer.core.utils;

import com.rdxer.core.ex.ToStringEx;
import com.rdxer.core.utils.search.SearchPara;
import com.rdxer.core.utils.search.WhereOperator;
import com.rdxer.core.utils.search.WhereUnit;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.util.StringUtils;

import javax.persistence.criteria.*;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

public class SearchUtils {

    /**
     * 直接调用即可
     * @param repository
     * @param para
     * @param <T>
     * @return
     */
    public static <T> Page<T> searchByPage(JpaSpecificationExecutor<T> repository, SearchPara para) {

        ToStringEx.toStringWithPrettyPrint(para);

        Pageable page = getPageable(para);
        Page<T> all = repository.findAll(new Specification<T>() {
            @Override
            public Predicate toPredicate(Root<T> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
                Predicate[] predicates = para.getWhere()
                        .entrySet()
                        .stream()
                        .filter(v -> v.getValue() != null)
                        .filter(v ->{
                            if (v.getValue() instanceof String ){
                                return !StringUtils.isEmpty((String)v.getValue());
                            }
                            return true;
                        })
                        .flatMap(v -> SearchUtils.makeWhereUnit(v.getKey(), v.getValue()).stream())
                        .map(v -> SearchUtils.makePredicate(v, root, cb))
                        .toArray(Predicate[]::new);
                return cb.and(predicates);
            }
        }, page);

        return all;
    }

    public static Pageable getPageable(SearchPara para) {
        Sort sort = getSort(para);

        return PageRequest.of(para.getPage() + para.offsetPage, para.getSize(), sort);
    }

    private static Sort getSort(SearchPara para) {
        return Sort.by(
                    para.getOrder()
                            .entrySet()
                            .stream()
                            .map(it -> new Sort.Order(Sort.Direction.fromString(it.getValue()), it.getKey()))
                            .collect(Collectors.toList())
            );
    }

    public static <T> List<T> searchAll(JpaSpecificationExecutor<T> repository, SearchPara para) {


        ToStringEx.toStringWithPrettyPrint(para);

        return repository.findAll(new Specification<T>() {
            @Override
            public Predicate toPredicate(Root<T> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
                Predicate[] predicates = para.getWhere()
                        .entrySet()
                        .stream()
                        .filter(v -> v.getValue() != null)
                        .filter(v ->{
                            if (v.getValue() instanceof String ){
                                return !StringUtils.isEmpty((String)v.getValue());
                            }
                            return true;
                        })
                        .flatMap(v -> SearchUtils.makeWhereUnit(v.getKey(), v.getValue()).stream())
                        .map(v -> SearchUtils.makePredicate(v, root, cb))
                        .toArray(Predicate[]::new);

                return cb.and(predicates);
            }
        },getSort(para));
    }


    public static WhereOperator makeOperator(Object v) {
        if (!(v instanceof String)){
            return WhereOperator.equle.setValue(v);
        }
        var valueStr = (String) v;
        boolean startsWith = valueStr.startsWith("[");
        if (startsWith) {
            int indexOf = valueStr.indexOf("]");
            if (indexOf != -1) {
                String substring = valueStr.substring(1, indexOf);
                WhereOperator parse = WhereOperator.parse(substring);
                if (parse != null) {
                    String substring1 = valueStr.substring(indexOf + 1);
                    return parse.setValue(substring1);
                }
            }
        }
        return WhereOperator.equle.setValue(valueStr);
    }

    public static List<WhereUnit> makeWhereUnit(String key, Object value) {
        List<WhereUnit> resp = new ArrayList<>();
        if (value instanceof String) {
            //  id":"[=]1000"
            String v = (String) value;
            WhereOperator operator = makeOperator(v);
            WhereUnit unit = new WhereUnit() {{
                setKey(key);
                setValue(operator.getValue());
                setOperator(operator);
            }};
            resp.add(unit);
        } else if (value instanceof List) {
            //  id":[]
            List<?> vs = (List<?>) value;
            for (Object v : vs) {
                // id":[]
                if (v instanceof List) { // id":[["=",1000],[]]
                    List<String> list = (List<String>) v;
                    String opt = list.get(0);
                    WhereOperator operator = WhereOperator.parse(opt);
                    WhereUnit unit = new WhereUnit() {{
                        setKey(key);
                        setValue(list.get(1));
                        setOperator(operator);
                    }};
                    resp.add(unit);
                    continue;
                }
                if (v instanceof Map) {// id":[{"=":1000},{}]
                    Map<String, Object> map = (Map<String, Object>) v;
//                    String opt = map.get("opt") + "";
//                    WhereOperator operator = WhereOperator.parse(opt);
//                    WhereUnit unit = new WhereUnit() {{
//                        setKey(key);
//                        setValue(map.get("value"));
//                        setOperator(operator);
//                    }};
//                    resp.add(unit);
                    makeWhereUnitWithValueMap(key, map, resp);
                    continue;
                }
                //if (v instanceof String) { // // id":[1000]   id":["1000"]

                    WhereOperator operator = makeOperator(v);
                    WhereUnit unit = new WhereUnit() {{
                        setKey(key);
                        setValue(operator.getValue());
                        setOperator(operator);
                    }};
                    resp.add(unit);
                    continue;
                //}
            }
        } else if (value instanceof Map) {
            //  id":{}
            makeWhereUnitWithValueMap(key, (Map<String, Object>) value, resp);
        } else {
            //  id":int/bool/
            WhereOperator operator = WhereOperator.equle.setValue(value);
            WhereUnit unit = new WhereUnit() {{
                setKey(key);
                setValue(operator.getValue());
                setOperator(operator);
            }};
            resp.add(unit);

        }
        return resp;
    }

    // id:{xxx}
    private static void makeWhereUnitWithValueMap(String key, Map<String, Object> value, List<WhereUnit> resp) {
        Map<String, Object> map = value;

        String opt = map.get("opt") + "";
        WhereOperator operator = WhereOperator.parse(opt);
        if (operator != null){
            // > "id":{ "opt":"in", "value":1000}

            Object v = map.get("value");
            WhereUnit unit = new WhereUnit() {{
                setKey(key);
                setValue(v);
                setOperator(operator);
            }};
            resp.add(unit);
        }else{
            // > "id":{">":1000,"<=":1003}
            for (Map.Entry<String, Object> entry : map.entrySet()) {
                var opt1 = WhereOperator.parse(entry.getKey());
                WhereUnit unit = new WhereUnit() {{
                    setKey(key);
                    setValue(entry.getValue());
                    setOperator(opt1);
                }};
                resp.add(unit);
            }
        }
    }

    public static Predicate makePredicate(WhereUnit unit, Root<?> root, CriteriaBuilder cb) {
        Object unitValue = unit.getValue();
        switch (unit.getOperator()) {
            case equle:
                return cb.equal(root.get(unit.getKey()), unitValue);
            case notEqule:
            case notEqule2:
                return cb.notEqual(root.get(unit.getKey()), unitValue);
            case like:
                return cb.like(root.get(unit.getKey()), unitValue + "");
            case notLike:
            case notLike2:
                return cb.notLike(root.get(unit.getKey()), unitValue + "");
            case lessThan:
                return cb.lessThan(root.get(unit.getKey()), unitValue + "");
            case lessThanOrEqualTo:
                return cb.lessThanOrEqualTo(root.get(unit.getKey()), unitValue + "");
            case greaterThan:
                return cb.greaterThan(root.get(unit.getKey()), unitValue + "");
            case greaterThanOrEqualTo:
                return cb.greaterThanOrEqualTo(root.get(unit.getKey()), unitValue + "");
//                cb.isNotNull()
//                cb.isNull()
//                cb.isTrue()
//                cb.isFalse()
            case isNotNull:
                return cb.isNotNull(root.get(unit.getKey()));
            case isNull:
                return cb.isNull(root.get(unit.getKey()));
            case isTrue:
                return cb.isTrue(root.get(unit.getKey()));
            case isFalse:
                return cb.isFalse(root.get(unit.getKey()));
            case in:
                try {
                    return makePredicateWithIn(unit, root, cb, unitValue);
                } catch (NoSuchMethodException e) {
                    e.printStackTrace();
                } catch (InvocationTargetException e) {
                    e.printStackTrace();
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                }
        }
        return null;
    }

    private static CriteriaBuilder.In makePredicateWithIn(WhereUnit unit, Root<?> root, CriteriaBuilder cb, Object unitValue) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
        Expression<?> exp = root.get(unit.getKey());
        CriteriaBuilder.In in = cb.in(exp);

        Class<?> expJavaType = exp.getJavaType();


        if (unitValue instanceof List) {
            List<?> vlist = (List<?>) unitValue;
            for (Object v : vlist) {
                Object result = v;
                if (!expJavaType.isInstance(v)) {
                    Method method;
                    try {
                        method = expJavaType.getMethod("valueOf", v.getClass());
                    } catch (NoSuchMethodException e) {
                        method = expJavaType.getMethod("valueOf", String.class);
                        v = v + "";
                    }
                    result = method.invoke(expJavaType, v);
                }
                in.value(result);
            }
        } else {
            Object result = unitValue;
            Object v = unitValue;
            if (!expJavaType.isInstance(unitValue)) {
                Method method;
                try {
                    method = expJavaType.getMethod("valueOf", v.getClass());
                } catch (NoSuchMethodException e) {
                    method = expJavaType.getMethod("valueOf", String.class);
                    v = v + "";
                }
                result = method.invoke(expJavaType, v);
            }
            in.value(result);
        }
        return in;
    }

    public static List<Predicate> makePredicate(String key, Object value, Root<?> root, CriteriaBuilder cb) {
        List<WhereUnit> units = makeWhereUnit(key, value);
        return units.stream().map(v -> makePredicate(v, root, cb)).collect(Collectors.toList());
    }


}
